/**
 *  @file InitOpenSSL.cpp
 */
#include "../../threading/Thread.h"
#include "../../threading/SyncLock.h"
#include "../../collections/ArrayT.h"
#include "../../util/AtomicOperations.h"
#include "../../exceptions/RuntimeException.h"
#include "../../common/__internal/InternalLock.h"
#include "InitOpenSSL.h"

#include "openssl/err.h" 
#include "openssl/rand.h" 
#include "openssl/ssl.h" 
#include "openssl/x509v3.h"
#include "openssl/crypto.h"

using namespace cppflib::collections;
using namespace cppflib::threading;
using namespace cppflib::exceptions;

/**
 *  structure declared in OpenSSL (but not defined) to create locks dynamically
 */
struct CRYPTO_dynlock_value 
{ 
    SyncLock *pLock;
};


namespace cppflib
{

namespace security
{

namespace __internal
{

//! init guard
PRIVATE volatile u32_t st_opensslInitializationCount = 0;
//! static locks
PRIVATE ArrayCppObjT<SyncLock> * st_lockArray = NULL;  

/**
 *  Create lock dynamically
 */
PRIVATE struct CRYPTO_dynlock_value * __DynamicCreateLock(const char *file, int line) 
{ 
    struct CRYPTO_dynlock_value *value = (struct CRYPTO_dynlock_value *)::MRP_Calloc(1, 
                                                                               sizeof(struct CRYPTO_dynlock_value)); 
    if (!value) 
        return NULL; 

    SyncLock * pLock = new SyncLock();
    cppmempool::UnlinkFromPool(pLock);
    value->pLock = pLock; 
    return value; 
}

/**
 *  Lock or Unlock for dynamic locks
 */
PRIVATE void __DynamicLockAndUnlock(int mode, struct CRYPTO_dynlock_value *l, const char *file, int line) 
{ 
    if (mode & CRYPTO_LOCK) 
        l->pLock->Lock(); 
    else 
        l->pLock->Unlock();
} 
  
/**
 *  Free lock for dynamic locks
 */
PRIVATE void __DynamicDestroyLock(struct CRYPTO_dynlock_value *l, const char *file, int line) 
{ 
    cppmempool::Release(l->pLock);
    cppmempool::Release(l); 
}


PRIVATE unsigned long __GetThreadIDCallback(void) 
{ 
    return static_cast<unsigned long>(Thread::GetCurrentThreadId()); 
} 

/**
 *  Lock or Unlock for static locks
 */
PRIVATE void __LockAndUnlockCallback(int mode, int n, const char * file, int line) 
{ 
    if (mode & CRYPTO_LOCK) 
        st_lockArray->Get(n)->Lock(); 
    else 
        st_lockArray->Get(n)->Unlock();
}

PRIVATE void __InitLocksForMultiThread(void)
{
   // init static locks
   u32_t nrStaticLocksNeeded = CRYPTO_num_locks();
   st_lockArray = new ArrayCppObjT<SyncLock>(nrStaticLocksNeeded);
   cppmempool::UnlinkFromPool(st_lockArray);

   for (int i = 0; i < static_cast<int>(st_lockArray->GetSize()); ++i) {
      SyncLock * pLock = new SyncLock();
      cppmempool::UnlinkFromPool(pLock);
      st_lockArray->Set(i, pLock);
      cppmempool::Release(pLock);
   }

   CRYPTO_set_id_callback(&__GetThreadIDCallback); 
   CRYPTO_set_locking_callback(&__LockAndUnlockCallback);

   // init callbacks for dynamic locks
   CRYPTO_set_dynlock_create_callback(&__DynamicCreateLock); 
   CRYPTO_set_dynlock_lock_callback(&__DynamicLockAndUnlock); 
   CRYPTO_set_dynlock_destroy_callback(&__DynamicDestroyLock);
}

PRIVATE void __UninitLocksForMultiThread(void)
{
   if (!st_lockArray)
      return;

   CRYPTO_set_id_callback(NULL); 
   CRYPTO_set_locking_callback(NULL);
   CRYPTO_set_dynlock_create_callback(NULL); 
   CRYPTO_set_dynlock_lock_callback(NULL); 
   CRYPTO_set_dynlock_destroy_callback(NULL);

   // release static locks
   cppmempool::Release(st_lockArray);
   st_lockArray = NULL;
}

/**
 *  Add seed to OpenSSL internal random number generator
 */
PRIVATE void __AddSeedToPRNG(void)
{

#if defined(WIN32)
   RAND_screen();
#elif defined(linux)
   
   const int BUFLEN = 32;
   u8_t buf[BUFLEN];

   // read random bytes from /dev/urandom or /dev/random
   FILE *fp = fopen("/dev/urandom", "rb");
   if (!fp) {
      fp = fopen("/dev/random", "rb");
   }

   if (!fp) {
      throw RuntimeException(_S("Cannot read random devices (/dev/urandom or /dev/random) to initialize random number generator"));
   }

   for (int i = 0; i < 32; ++i) {
      if (fread(buf, 1, BUFLEN, fp)) { } // to avoid compiler warning
      RAND_seed(buf, BUFLEN);
   }

   fclose(fp);

#endif
}


/**
 *  Initialize Openssl env
 */
PRIVATE void __InitOpenSSL(void)
{
   if (util::AtomicIncrementFetch(&st_opensslInitializationCount) > 1)
      return;

   __InitLocksForMultiThread();

   if (!SSL_library_init())
      throw RuntimeException(_S("OpenSSL initialization failed")); // this should crash the program

   SSL_load_error_strings();

   __AddSeedToPRNG();
}

/**
 *  Release resource allocated above
 */
PRIVATE void __UninitOpenSSL(void)
{
   if (util::AtomicDecrementFetch(&st_opensslInitializationCount) > 0)
      return;

   // uninit Openssl stuff
   CONF_modules_unload(1);
   ERR_remove_state(0);
   ERR_free_strings(); // free strings loaded by SSL_load_error_strings
   OBJ_cleanup();
   EVP_cleanup();
   CRYPTO_cleanup_all_ex_data();
   COMP_zlib_cleanup();

   // this stack is allocated by 'SSL_library_init'
   SSL_COMP_free_compression_methods();

   __UninitLocksForMultiThread();
}

void InitOpenSSL(void)
{
   INTL_AcquireLock(LP_SSLRESOURCE);
   __InitOpenSSL();
   INTL_ReleaseLock(LP_SSLRESOURCE);
}

void UninitOpenSSL(void)
{
   INTL_AcquireLock(LP_SSLRESOURCE);
   __UninitOpenSSL();
   INTL_ReleaseLock(LP_SSLRESOURCE);
}

} // end of namespace __internal

} // end of namespace security

} // end of namespace cppflib

